package cn.seaboot.commons.net;

import cn.seaboot.commons.core.CommonUtils;
import cn.seaboot.commons.core.Converter;
import cn.seaboot.commons.lang.NameValueEntity;
import cn.seaboot.commons.lang.NameValuePair;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Builder for {@link URI} instances.
 * <p>
 * 以路径 http://127.0.0.1:8080/api?a=1&b=2 为例
 */
public class UriBuilder {

    private UriBuilder(){}

    public static UriBuilder create(){
        return new UriBuilder();
    }

    public static UriBuilder create(URI uri){
        return new UriBuilder().setUri(uri);
    }

    public static UriBuilder create(String uri){
        return new UriBuilder().setUri(URI.create(uri));
    }

    private static final char QUERY_PARAM_SEPARATOR = '&';
    private static final char PARAM_VALUE_SEPARATOR = '=';
    private static final char PATH_SEPARATOR = '/';
    /**
     * 地址，例如：127.0.0.1:8080、www.baidu.com
     * <p>
     * 等价于：userInfo@host:port
     */
    private String authority;
    /**
     * 采用的协议，例如：http
     */
    private String scheme;
    /**
     * 主机，例如：127.0.0.1、www.baidu.com
     */
    private String host;
    /**
     * 端口，例如：8080
     */
    private int port;
    /**
     * 地址
     * <p>
     * 例如：http://127.0.0.1:8080/api?a=1&b=2#abc 中的 api
     */
    private String path;
    /**
     * 查询参数，例如：a=1&b=2
     */
    private String query;
    /**
     * 编码，例如：UTF-8
     */
    private Charset charset;
    /**
     * 次级资源，VUE 项目很常见这种地址
     * <p>
     * 例如：http://127.0.0.1:8080/api?a=1&b=2#abc 结尾 # 好后面的 abc
     */
    private String fragment;
    /**
     * 账号密码，ftp地址中很常见
     * <p>
     * 例如：http://use:pwd@127.0.0.1:8080/api?a=1&b=2#abc中的 use:pwd
     */
    private String userInfo;

    /**
     * HashMap 不是一个合适的数据结构
     * <p>
     * 请求参数是允许出现多个同名参数，如果出现同名参数，则
     */
    private List<NameValueEntity> params;

    /**
     * Sets URI query parameters. The parameter name / values are expected to be unescaped
     * and may contain non ASCII characters.
     * <p>
     * Please note query parameters and custom query component are mutually exclusive. This method
     * will remove custom query if present.
     * </p>
     * http 请求中，允许存在同名参数，用 map 并不符合规范，但大部分场景下，这个函数非常有用
     *
     * @param params 请求参数
     * @param charset value 采用的编码
     * @return this.
     */
    public UriBuilder setParameters(final Map<String, Object> params, String charset) {
        if (this.params == null) {
            this.params = new ArrayList<>();
        } else {
            this.params.clear();
        }
        if(CommonUtils.isNotEmpty(params)){
            for (Map.Entry<String, ?> entry : params.entrySet()) {
                this.addValue(entry.getKey(), entry.getValue(), charset);
            }
        }
        this.query = null;
        return this;
    }

    /**
     * Sets URI query parameters. The parameter name / values are expected to be unescaped
     * and may contain non ASCII characters.
     * <p>
     * Please note query parameters and custom query component are mutually exclusive. This method
     * will remove custom query if present.
     * </p>
     *
     * @return this.
     */
    public UriBuilder setParameters(final List<NameValueEntity> entities, String charset) {
        if (this.params == null) {
            this.params = new ArrayList<>();
        } else {
            this.params.clear();
        }
        if(CommonUtils.isNotEmpty(entities)){
            for (NameValueEntity entity : entities) {
                this.addValue(entity.getName(), entity.getValue(), charset);
            }
        }
        this.query = null;
        return this;
    }

    /**
     * Sets parameter of URI query overriding existing value if set. The parameter name and value
     * are expected to be unescaped and may contain non ASCII characters.
     * <p>
     * Please note query parameters and custom query component are mutually exclusive. This method
     * will remove custom query if present.
     * </p>
     *
     * @return this.
     */
    public UriBuilder setParameter(final String name, final Object value, String charset) {
        if (this.params == null) {
            this.params = new ArrayList<>();
        } else {
            this.params.clear();
        }
        this.addValue(name, value, charset);
        this.query = null;
        return this;
    }

    /**
     * Sets URI query parameters. The parameter name / values are expected to be unescaped
     * and may contain non ASCII characters.
     * <p>
     * Please note query parameters and custom query component are mutually exclusive. This method
     * will remove custom query if present.
     * </p>
     *
     * @return this.
     */
    public UriBuilder addParameters(final List<NameValueEntity> entities, String charset) {
        if (this.params == null) {
            this.params = new ArrayList<>();
        }
        if(CommonUtils.isNotEmpty(entities)){
            for (NameValueEntity entity : entities) {
                this.addValue(entity.getName(), entity.getValue(), charset);
            }
        }
        this.query = null;
        return this;
    }

    /**
     * Sets URI query parameters. The parameter name / values are expected to be unescaped
     * and may contain non ASCII characters.
     * <p>
     * Please note query parameters and custom query component are mutually exclusive. This method
     * will remove custom query if present.
     * </p>
     * http 请求中，允许存在同名参数，用 map 并不符合规范，但大部分场景下，这个函数非常有用
     *
     * @return this.
     */
    public UriBuilder addParameters(final Map<String, Object> map, String charset) {
        if (this.params == null) {
            this.params = new ArrayList<>();
        }
        if(CommonUtils.isNotEmpty(map)){
            for (Map.Entry<String, ?> entry : map.entrySet()) {
                this.addValue(entry.getKey(), entry.getValue(), charset);
            }
        }
        this.query = null;
        return this;
    }

    /**
     * Adds parameter to URI query. The parameter name and value are expected to be unescaped
     * and may contain non ASCII characters.
     * <p>
     * Please note query parameters and custom query component are mutually exclusive. This method
     * will remove custom query if present.
     * </p>
     *
     * @return this.
     */
    public UriBuilder addParameter(final String name, final Object value, String charset) {
        if (this.params == null) {
            this.params = new ArrayList<>();
        }
        this.addValue(name, value, charset);
        return this;
    }

    private void addValue(final String name, final Object value, String charset) {
        try {
            String val = Converter.toString(value);
            val = URLEncoder.encode(val, charset);
            this.params.add(new NameValueEntity(name, val));
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("Unsupported encoding parameter: " + name + "." + value, e);
        }
    }

    /**
     * Removes parameter of URI query if set. The parameter name is expected to be unescaped and may
     * contain non ASCII characters.
     * <p>
     * Please note query parameters and custom query component are mutually exclusive. This method
     * will remove custom query if present, even when no parameter was actually removed.
     * </p>
     *
     * @return this.
     * @since 5.2
     */
    public UriBuilder removeParameter(final String name) {
        if (CommonUtils.isNotEmpty(params)) {
            params.removeIf(entity -> entity.getName().equals(name));
        }
        return this;
    }

    /**
     * Sets parameter of URI query overriding existing value if set. The parameter name and value
     * are expected to be unescaped and may contain non ASCII characters.
     * <p>
     * Please note query parameters and custom query component are mutually exclusive. This method
     * will remove custom query if present.
     * </p>
     *
     * @return this.
     */
    public UriBuilder setParameter(final String param, final String value, String charset) throws UnsupportedEncodingException {
        if (this.params == null) {
            this.params = new ArrayList<>();
        }
        if (!this.params.isEmpty()) {
            this.addParameter(param, value, charset);
        }
        return this;
    }

    /**
     * Clears URI query parameters.
     *
     * @return this.
     */
    public UriBuilder clearParameters() {
        this.params.clear();
        return this;
    }

    void formatQuery(final StringBuilder sb, final Iterable<? extends NameValuePair> params, String charset) {
        int i = 0;
        for (final NameValuePair parameter : params) {
            if (i > 0) {
                sb.append(QUERY_PARAM_SEPARATOR);
            }
            sb.append(parameter.getName());
            if (parameter.getValue() != null) {
                sb.append(PARAM_VALUE_SEPARATOR).append(parameter.getValue());
            }
            i++;
        }
    }

    private String buildString() {
        final StringBuilder sb = new StringBuilder();
        if (this.scheme != null) {
            sb.append(this.scheme).append(':');
        }
        if (this.authority != null) {
            sb.append("//").append(this.authority);
        } else if (this.host != null) {
            sb.append("//");
            if (this.userInfo != null) {
                sb.append(this.userInfo).append("@");
            }
            sb.append(this.host);
            if (this.port >= 0) {
                sb.append(":").append(this.port);
            }
        }
        if (this.path != null) {
            if (!this.path.startsWith("/")) {
                sb.append(PATH_SEPARATOR);
            }
            sb.append(this.path);
        }
        if (this.query != null) {
            sb.append("?").append(this.query);
        } else if (this.params != null && !this.params.isEmpty()) {
            sb.append("?");
            formatQuery(sb, this.params, this.charset.name());
        }
        if (this.fragment != null) {
            sb.append("#").append(this.fragment);
        }
        return sb.toString();
    }

    /**
     * Builds a {@link URI} instance.
     */
    public URI build() throws URISyntaxException {
        return new URI(this.buildString());
    }

    @Override
    public String toString() {
        return this.buildString();
    }

    // getter / setter

    public UriBuilder setUri(URI uri) {
        this.setScheme(uri.getScheme());
        this.setUserInfo(uri.getUserInfo());
        this.setHost(uri.getHost());
        this.setPort(uri.getPort());
        this.setPath(uri.getPath());
        this.setQuery(uri.getQuery());
        this.setFragment(uri.getFragment());

        this.setAuthority(uri.getAuthority());
        return this;
    }

    public String getAuthority() {
        return authority;
    }

    public UriBuilder setAuthority(String authority) {
        this.authority = authority;
        return this;
    }

    public String getScheme() {
        return scheme;
    }

    public UriBuilder setScheme(String scheme) {
        this.scheme = scheme;
        return this;
    }

    public String getUserInfo() {
        return userInfo;
    }

    public UriBuilder setUserInfo(String userInfo) {
        this.userInfo = userInfo;
        return this;
    }

    public String getHost() {
        return host;
    }

    public UriBuilder setHost(String host) {
        this.host = host;
        return this;
    }

    public int getPort() {
        return port;
    }

    public UriBuilder setPort(int port) {
        this.port = port;
        return this;
    }

    public String getPath() {
        return path;
    }

    public UriBuilder setPath(String path) {
        this.path = path;
        return this;
    }

    public String getQuery() {
        return query;
    }

    public UriBuilder setQuery(String query) {
        this.query = query;
        return this;
    }

    public Charset getCharset() {
        return charset;
    }

    public UriBuilder setCharset(Charset charset) {
        this.charset = charset;
        return this;
    }

    public String getFragment() {
        return fragment;
    }

    public UriBuilder setFragment(String fragment) {
        this.fragment = fragment;
        return this;
    }
}
